package org.jugile.daims;

/*

Copyright (C) 2011-2011 Jukka Rahkonen  email: jukka.rahkonen@iki.fi

This library is free software; you can redistribute it and/or
modify it under the terms of the GNU Lesser General Public
License as published by the Free Software Foundation; either
version 2.1 of the License, or (at your option) any later version.

This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
Lesser General Public License for more details.

You should have received a copy of the GNU Lesser General Public
License along with this library; if not, write to the Free Software
Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA

*/

//import gnu.trove.iterator.TLongObjectIterator;

import java.lang.reflect.Field;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import org.apache.log4j.Logger;
import org.jugile.daims.anno.Connection1N;
import org.jugile.daims.anno.ConnectionN1;
import org.jugile.daims.anno.ConnectionNN;
import org.jugile.daims.anno.DaimsObject;
import org.jugile.daims.anno.Fld;
import org.jugile.util.Buffer;
import org.jugile.util.DBConnection;
import org.jugile.util.DBPool;
import org.jugile.util.DOH;
import org.jugile.util.EnumType;
import org.jugile.util.HiLo;
import org.jugile.util.IDO;
import org.jugile.util.IDomain;
import org.jugile.util.Jugile;
import org.jugile.util.Money;
import org.jugile.util.Proxy;
import org.jugile.util.Time;

/**
 * <i>"this is verse"</i> <b>()</b>
 * 
 * <br/>==========<br/>
 *
 * here is doc
 *  
 * @author jukka.rahkonen@iki.fi
 *
 */
public class Bo extends Jugile implements Cloneable, Comparable<Bo>, IDO {
	protected static Logger log = Logger.getLogger(Bo.class);

	private UnitOfWork uow() { return DomainCore.getUnitOfWork(); }
	
	// ----------- id & version & equals -------------
	private long id;
	public long id() { return id; }
	public String getId() { return ""+id; }

	/**
	 * Create "old" means creating already existing object: it has an id already.
	 * This is the case when object is imported from delta or from initial import.
	 * Returns an object that was born old.
	 */
	protected static Bo createOld(Class<? extends Bo> cl, long id) {
		Bo o = getInstance(cl);
		o.id = id;
		return o;
	}

	/**
	 * Creates a new virgin object and aquires new id for it from idpool.
	 */
	protected static Bo createNew(Class<? extends Bo> cl) {
		Bo o = getInstance(cl);
		o.id = HiLo.nextid();
		o.isNew = true;
		return o;
	}
	
	private static Bo getInstance(Class<? extends Bo> cl) {
		try { return cl.newInstance();
		} catch (Exception e) { fail(e); return null; }
	}
	
	private long version;
	protected long version() { return version; }
	private void incrVersion() { version++; }
	public long getVersion() { return version; }

	private Bo origin;
	protected boolean isOrigin() { return origin == null; }
	protected boolean isCopy() { return origin != null; }
	
	private boolean modified = false;
	protected boolean isModified() { return modified; }
	
	private boolean isNew = false;
	protected boolean isNew() { return isNew; }
	protected void setIsNew(boolean v) { isNew = v; }

	private boolean isArchived = false;
	public boolean isArchived() { return isArchived; }

	protected boolean isRef() { return version < 0; }
	private Bo createRef() {
		Bo o = Bo.createOld(this.getClass(), id);
		o.version = -1;
		o.origin = this;
		return o;
	}

	protected Bo copy(BoInfo bi) {
		if (uow().isReadOnly()) return this;
		if (isCopy()) fail("tried to copy a copy");
		// no cloning for to avoid cloning automatically all referenced objects (Bo member fields)
		// create empty copied object
		Bo o = Bo.createOld(this.getClass(), id);
		return copy(bi,o);
	}
	
	protected Bo copy(BoInfo bi, Bo o) {
		o.version = version;
		o._modified = _modified;
		try {
			// copy fields
			for (Field f : bi.flds) {
				f.set(o, f.get(this));
			}
			
			// copy Bo n-1 refs
			for (Field f : bi.cn1s) {
				Bo bo = (Bo)f.get(this);
				if (bo != null) {
					f.set(o, bo.createRef());
				}
			}
			
			// copy all collections
			for (Field f : bi.cols) {
				// create BoMapDelta from BoMap
				BoCollection<Bo> bc = (BoCollection<Bo>)f.get(this);
				BoCollection<Bo> bc2 = (BoCollection)f.getType().newInstance();
				bc2.setOrigin(bc);
				f.set(o,bc2);
			}			
			
		} catch (Exception e) { fail(e); }
		if (origin == null) o.origin = this;
		return o;
	}
	
	/**
	 * Gets the version which is in uow or creates a copy.
	 * @return uow copy.
	 */
	protected Bo getUowCopy() {
		return uow().get(getClass(), id);
	}
	
	
	protected BoMap<Bo> getBoMap(String name) throws Exception {
		Field f = fld(name);
		BoCollection<Bo> bc = (BoCollection<Bo>)f.get(this);
		return bc.origin;
	}
	
	protected BoCollection<Bo> getBoCollection(String name) throws Exception {
		Field f = fld(name);
		BoCollection<Bo> bc = (BoCollection<Bo>)f.get(this);
		return bc;
	}
	
	
	public int hashCode() { return Long.valueOf(id).hashCode(); }
	public boolean equals(Object o) {
		if (o == null) return false;
		if (!(o instanceof Bo)) return false;
		return ((Bo)o).id == id;
	}
	
	public int compareTo(Bo o) {
		if (o == null) return 1;
		return getId().compareTo(o.getId());
	}

	
	// ----------- flds ------------
	protected Bo setFld(String name, Object value) {
		try {
			Field f = fld(name);
			f.set(this, value);
			modified = true;
			_modified = now();
		} catch(Exception e) { fail(e); }
		return this;
	}

	protected Bo getN1(String name) {
		try {
			Field f = fld(name);
			Bo value = (Bo)f.get(this);
			if (value == null) return null;
			if (value.isRef()) {
				// lazy load
				Bo val = uow().get((Class<Bo>)value.getClass(), value.id);
				f.set(this, val); 
				return val;
			}
			return value;
		} catch (Exception e) { fail(e); }
		return null;
	}
	
	protected Bo setN1(String name, Bo o) {
		try {
			Field f = fld(name);
			setDeltaN1(f,o); // set n-1 collection
			modified = true;
			_modified = now();
		} catch(Exception e) { fail(e); }
		return this;
	}

	private void setDeltaN1(Field f, Bo o) throws Exception {
		// remove this from old index
		//log.debug("setDeltaN1: " + f.getName() + " o: " + o);
		Bo oldvalue = (Bo)f.get(this);
		if (oldvalue != null) {
			// for lazyloaded objects
			if (oldvalue.isRef()) {
				oldvalue = uow().get((Class<Bo>)oldvalue.getClass(), oldvalue.id);
			}
		}
		if (eq(oldvalue,o)) {
			//log.debug("oldvalue eq o: " + o);
            return; // no change			
		}
		if (oldvalue != null) {
			// must use unit of work here to get collection
			oldvalue = uow().get((Class<Bo>)oldvalue.getClass(), oldvalue.id);
			// if removed in uow -> oldvalue is now null
			if (oldvalue != null) {
				BoCollection<Bo> bc = oldvalue.getBoCollection(f.getAnnotation(ConnectionN1.class).c());
				bc.remove(this,false);
				//String status = bc.status();
			}
			//log.debug("deleted o from: " + bc);
		}
		// add this into new index
		if (o != null) {
			BoCollection<Bo> bc = o.getBoCollection(f.getAnnotation(ConnectionN1.class).c());
			bc.add(this);
		}
		// set ref field value
		f.set(this,o);
	}

	private void setOriginN1(Field f, Bo o) throws Exception {
		if (!isOrigin()) fail("tried to setOriginN1 to copy");
		if (o != null) if (!o.isOrigin()) fail("tried to add copy to origin");
		//log.debug("setOriginN1: "+ toStr(f) + o + "this: " +this);
		// remove this from old index
		Bo oldvalue = (Bo)f.get(this);
		if (eq(oldvalue,o)) {
			//log.debug("oldvalue eq o " + this);
			return; // no change
		}
		if (oldvalue != null) {
			BoMap<Bo> bm = oldvalue.getBoMap(f.getAnnotation(ConnectionN1.class).c());
			//log.debug("removing from other end: " + id + " bm: " +bm);
			bm.remove(id);
		}
		// add this into new index
		if (o != null) {
			BoMap bm = o.getBoMap(f.getAnnotation(ConnectionN1.class).c());
			bm.add(this);
		}
		// set ref field value
		f.set(this,o);
		//log.debug("setOriginN1 ok: " +o + "this: " + this);
	}
	
	protected BoCollection getAll(String name) {
		try {
			Field f = fld(name);
			BoCollection value = (BoCollection)f.get(this);
			value.reset(); // remove old queries and iterator
			//log.debug("getAll from bomap: " + value.origin + "this: " + this);
			return value;
		} catch (Exception e) { fail(e); }
		return null;
	}

	protected Bo add(String name, Bo o) {
		if (o == null) return this;
		if (o.isOrigin()) {
			log.warn("tried to add origin: " + o);
			o = uow().get(o.getClass(), o.id);
		}		
		try {
			Field f = fld(name);
			Connection1N c = f.getAnnotation(Connection1N.class);
			if (c != null) {
				o.setN1(c.o(), this); // use other end setter
				return this;
			}
			ConnectionNN nn = f.getAnnotation(ConnectionNN.class);
			if (nn != null) {
				((BoCollection<Bo>)f.get(this)).add(o);
				o.getBoCollection(nn.c()).add(this); // update other end index
				return this;
			}
		} catch(Exception e) { fail(e); }
		return this;
	}
	
	protected void remove(String name, Bo o) {
		if (o == null) return;
		if (o.isOrigin()) {
			log.warn("tried to remove origin: " + o);
			o = uow().get(o.getClass(), o.id);
		}
		try {
			Field f = fld(name);
			Connection1N c = f.getAnnotation(Connection1N.class);
			if (c != null) {
				//log.debug("remove 1N by calling o.setN1");
				o.setN1(c.o(), null); // use other end setter and set null
				return;
			}
			ConnectionNN nn = f.getAnnotation(ConnectionNN.class);
			if (nn != null) {
				//log.debug("remove NN: " + o + " " + name);
				((BoCollection<Bo>)f.get(this)).remove(o,false);
				o.getBoCollection(nn.c()).remove(this,false); // update other end index
				return;
			}
		} catch(Exception e) { fail(e); }
		return;
	}
	
	protected void cleanUpConnections() throws Exception {
		//log.debug("cleanUp: " + this);
		for (Field f : bi().cols) {
			// foreach item do remove
			BoCollection<Bo> bc = (BoCollection<Bo>)f.get(this);
			//log.debug("cleanUp: " + f.getName() + " "+bc);
			for (Bo o : bc) {
				// TODO: optimize this
				// get it to uow
				Bo uo = uow().get(o.getClass(), o.id());
				remove(f.getName(),uo); // cleans up other end
			}
		}
		for (Field f : bi().cn1s) {
			setN1(f.getName(), null);
		}		
	}
	
	protected void cleanUpOriginConnections() throws Exception {
		//log.debug("cleanUpOrigin: " + this);
		for (Field f : bi().nns) {
			BoCollection<Bo> bc = (BoCollection<Bo>)f.get(this);
			for (Bo o : bc) {
				removeOriginNN(f.getName(), o.id);
			}
		}
		for (Field f : bi().c1ns) {
			BoCollection<Bo> bc = (BoCollection<Bo>)f.get(this);
			String oname = f.getAnnotation(Connection1N.class).o();
			for (Bo o : bc) {
				o.origin.setFld(oname,null);
			}
		}
		for (Field f : bi().cn1s) {
			//log.debug("set null: " + f.getName());
			setOriginN1(f, null);
		}		
	}

	protected void addOriginNN(String name, Bo o) {
		if (o == null) return;
		try {
			Field f = fld(name);
			ConnectionNN nn = f.getAnnotation(ConnectionNN.class);
			if (nn == null) fail("not a nn collection");
			getBoMap(name).add(o);
			o.getBoMap(nn.c()).add(this); // update other end index
		} catch(Exception e) { fail(e); }
	}
	
	protected void removeOriginNN(String name, long id) {
		try {
			//log.debug("removeOriginNN: " + name + " id: " + id);
			Field f = fld(name);
			ConnectionNN nn = f.getAnnotation(ConnectionNN.class);
			if (nn == null) fail("not a nn collection");
			Bo o = getBoMap(name).remove(id);
			if (o != null) o.getBoMap(nn.c()).remove(this); // update other end index			
		} catch(Exception e) { fail(e); }
	}

	protected Class getOtherEndClass(String name) {
		try {
			Field f = fld(name);
			ConnectionNN nn = f.getAnnotation(ConnectionNN.class);
			if (nn == null) fail("not a nn collection");
			int l = "Collection".length();
			String cname = f.getType().getName();
			cname = cname.substring(0, cname.length()-l);
			return getClassByName(cname);
		} catch(Exception e) { fail(e); return null; }
	}
	
	
	enum FldType { FIELD, CN1, C1N, NN, COL }	
	protected List<Field> flds() { return flds(FldType.FIELD); }
	protected List<Field> cn1s() { return flds(FldType.CN1); }
	protected List<Field> c1ns() { return flds(FldType.C1N); }
	protected List<Field> nns() { return flds(FldType.NN); }
	protected List<Field> cols() { return flds(FldType.COL); }

	protected List<Field> flds(FldType type) {
		List<Field> res = new ArrayList<Field>();
		Class cl = this.getClass().getSuperclass();
		for (Field f : cl.getDeclaredFields()) {
			if (type == FldType.FIELD) {
				Fld fa = f.getAnnotation(Fld.class);
				if (fa == null) continue;
			}
			if (type == FldType.CN1) {
				ConnectionN1 c = f.getAnnotation(ConnectionN1.class);
				if (c == null) continue;
			}
			if (type == FldType.C1N) {
				Connection1N c = f.getAnnotation(Connection1N.class);
				if (c == null) continue;				
			}
			if (type == FldType.NN) {
				ConnectionNN c = f.getAnnotation(ConnectionNN.class);
				if (c == null || !c.first()) continue;
				//if (c == null || c.first()) continue;
			}
			if (type == FldType.COL) {
				Connection1N c = f.getAnnotation(Connection1N.class);
				ConnectionNN nn = f.getAnnotation(ConnectionNN.class);
				if (c == null && nn == null) continue;
			}
			f.setAccessible(true);
			res.add(f);
		}
		return res;
	}


	private Field fld(String name) throws Exception {
		Field f = this.getClass().getSuperclass().getDeclaredField(name);
		f.setAccessible(true);
		return f;
	}
	
	// ------------ csv ---------------
	
	protected final static String TSFORMAT = "yyyyMMdd-HHmmss.SSS";
	protected final static char CSVDELIMITER = ',';
	protected final static char NULLCHAR = '-';
	protected final static String DEL = "D";
	protected final static String HEADERSTART = "#";
	protected final static String MAPSTART = "@";
	
	
	public String toCsv() { return toCsv(0);}
	private final String toCsv(int versionIncrement) {
		Buffer buf = new Buffer();
		buf.add(getId()); // id
		buf.add(CSVDELIMITER);
		buf.add(""+(version+versionIncrement)); // version
		buf.add(CSVDELIMITER);
		buf.add(_modified==null?"-":_modified.toString(TSFORMAT)); // modified time
		List<Field> flds = flds();
		flds.addAll(cn1s());
		for (Field f : flds) {
			buf.add(CSVDELIMITER);
			Object o = null;
			try { o = f.get(this); } catch (Exception e) { fail(e); }
			if (o == null) buf.add(NULLCHAR);
			else if (o instanceof String) {
				if (((String)o).length() == 0) buf.add(NULLCHAR);
				else if (((String)o).equals(""+NULLCHAR)) {
					buf.add(""+NULLCHAR+NULLCHAR);
				}
				else {
					String str = Jugile.replaceControls((String)o);
					str = escape(str,CSVDELIMITER);
					buf.add(str);
				}
			}
			else if (o instanceof Bo) buf.add(((Bo)o).getId());
			else if (o instanceof Time) buf.add(((Time)o).toString(TSFORMAT));
			else if (o instanceof EnumType) buf.add(""+((EnumType)o).getValue());
			else buf.add(escape(o.toString(),CSVDELIMITER));
		}
		return buf.toString();
	}

	protected final void parse(DomainData dd, List<String> flds, List<String> values) throws Exception {
		if (!isOrigin()) fail("tried to parse into copy");
		// skip id(0)
		long v = parseLongSafe(values.get(1)); // verify version - don't update older version
		if (version > 0) if (v < version) return;
		version = v;
		if ("-".equals(values.get(2))) _modified = null;
		else _modified = new Time(values.get(2),TSFORMAT);	
		
		for (int i = 3; i < flds.size(); i++) {
			String val = null;
			try {
				val = values.get(i);
			} catch (Exception e) {
				log.error("ERROR: " + e);
				log.error("  " + join(flds) + " = " + join(values));
			}
			//log.debug("parse " + id() +": "+ flds.get(i) + "=" + val);
			if (empty(val)) continue;
			String fld = flds.get(i);
			Field f = fld(fld);
			if (val.equals(""+NULLCHAR)) val = null;
			Object o = null;
			if (f.getAnnotation(Fld.class) != null) {
				if (val != null) {
					if (f.getType() == Time.class) {
						o = new Time(val,TSFORMAT);
					} else if (f.getType() == Money.class) {
						o = new Money(parseIntSafe(val));
					} else if (f.getType() == String.class) {
						if (val.equals(""+NULLCHAR+NULLCHAR)) val = ""+NULLCHAR;
						o = val;
					} else if (f.getType() == int.class) {
						o = parseIntSafe(val);
					} else if (f.getType() == boolean.class) {
						o = parseBoolSafe(val);
					} else if (f.getType() == double.class) {
						o = parseDoubleSafe(val);
					} else if (hasInterface(f.getType(),EnumType.class)) {
						o = Proxy.staticCall(f.getType(), "map", val);
					} else {
						fail("unknown type: " + f.getType());
					}
				} else {
					if (f.getType() == int.class) o = 0;
					else if (f.getType() == double.class) o = 0.0;
				}
				f.set(this,o);
			} else if (f.getAnnotation(ConnectionN1.class) != null) {
				Bo bo = dd.getOrCreate((Class<Bo>)f.getType(), val);
				//log.debug("setting OriginN1: " + toStr(f) + bo + "this: " + this);
				this.setOriginN1(f, bo);
			} else {
				fail("invalid field: " + f);
			}
		}
		// TODO: this causes ANG loadCsv to work incorrectly
		if (dd != null && dd.d() != null)
			onLoad((IDomain)dd.d()); // call onLoad
	}
	
	private boolean hasInterface(Class cl, Class ic) {
		for (Class i : cl.getInterfaces()) {
			if (i.equals(ic)) return true;
		}
		return false;
	}
	protected final String _headerRow() {
		Buffer buf = new Buffer();
		buf.add(Bo.HEADERSTART);
		buf.add(getClass().getName());
		buf.add(Bo.CSVDELIMITER);
		buf.add("v");
		buf.add(Bo.CSVDELIMITER);
		buf.add("ts");
		List<Field> flds = flds();
		flds.addAll(cn1s());
		for (Field f : flds) {
			buf.add(Bo.CSVDELIMITER);
			buf.add(f.getName());
		}
		return buf.toString();
	}
	
	protected final String _deleteRow() {
		// version incremented also here, version is incremented in delta batch
		//return DEL + CSVDELIMITER + id + CSVDELIMITER + (version+1);
		return DEL + CSVDELIMITER + id + CSVDELIMITER + version;
	}	
	
	protected String _delta() {
		// TODO: optimize: calculate real delta - only modified fields
		onSave(); // callback, TODO: make this call separately before delta() round
		return toCsv(1); // increment version in delta
	}

	protected String _nnHeader(Field f) {
		String clName = getClass().getName();
		String cn1 = f.getName();
		//String cn2 = f.getAnnotation(ConnectionNN.class).c(); 
		return MAPSTART + CSVDELIMITER + clName + CSVDELIMITER + cn1;		
	}

	public String toString() {
		// TODO: truncate long text fields 
		String res = "[Bo "+ getClassName(this) + " "+ hex(super.hashCode()) + " "+ toCsv()+"] ";
		if (origin != null) res += "origin-> " + origin.toString();
		return res;
	}

	public static String toStr(Field f) {
		return "[Fld " + f.getName() +":" + getClassName(f.getType()) +"] ";
	}
	
	protected void dump(Buffer buf) throws Exception {
		// 5430D082  Person (2,1,0,-,p1,-)
		//   family: 5430D082  Family (2,1,0) ->  5430D082  
		buf.ln(toRefStr());
		buf.incrIndent();
		for (Field f : cn1s()) {
			Bo bo = (Bo)f.get(this);
			buf.ln(f.getName() +": " + (bo==null?"null":bo.toRefStr()));			
		}
		//    persons( 5430D082 -> 5430D082 ):
		//        items: 5430D082, 5430D082, 5430D082, 5430D082
		//        deleted: 5430D082, 5430D082  
		for (Field f : cols()) {
			BoCollection dm = (BoCollection<Bo>)f.get(this);
			BoMap orig = dm.origin;
			String ref1 = hex(dm.hashCode());
			String ref2 = hex(orig.hashCode());
			buf.ln(f.getName() + "( " + ref1 + " -> " + ref2 + " ):");
			buf.incrIndent();
			if (isOrigin()) {
				orig.dumpShort(buf);
			} else {
				dm.dumpShort(buf);
			}
			buf.decrIndent();
		}
		buf.decrIndent();
	}
	
	protected String toRefStr() {
		// 5430D082  Family (2,1,0) ->  5430D082  
		String res = realHash();
		res += " " + getClassName(this)+" ("+toCsv()+") ";
		if (origin != null) res += " -> " + origin.toRefStr();
		return res;
	}
	
	protected String realHash() { return hex(super.hashCode()); }

	
	// ---- db -----
	
	private List<String> _dbflds() {
		List<String> res = new ArrayList<String>();

		for (Field f : flds()) {
			String name = f.getAnnotation(Fld.class).name();
			if (empty(name)) name = f.getName() + "_f";
			res.add(name);
		}
		for (Field f : cn1s()) {
			String name = f.getAnnotation(ConnectionN1.class).name();
			if (empty(name)) name = f.getName() + "_f";
			res.add(name);			
		}
		return res;
	}
	
	private final static String sHeaderFlds[] = {
		"id_f","ts","host","vers","archived"
	};
	private int countSelectHeaderFlds() {
		return sHeaderFlds.length;
	}
	private String getSelectHeaderFldsString() {
		return Jugile.join(Arrays.asList(sHeaderFlds), ",");
	}
	protected String _getSelectFlds() {
		Buffer buf = new Buffer();
		buf.add(getSelectHeaderFldsString());
		for (String fld : _dbflds()) {
			buf.add(","+fld);
		}
		return buf.toString();
	}
	protected String _getUpdateFlds() {
		Buffer buf = new Buffer();
		for (int i = 0; i < _dbflds().size(); i++) {
			if (i > 0) buf.add(",");
			buf.add(_dbflds().get(i)+"=?");
		}
		return buf.toString();		
	}

	protected void _setWriteParams(DBConnection c, BoInfo bi) throws Exception {
		int i = 0;
		for (Field f : bi.flds) {
			int size = bi.sizes.get(i);
			if (size > 0) c.param(trim((String)f.get(this),size));
			else c.param(f.get(this));
			i++;
		}
		for (Field f : bi.cn1s) {
			c.param(f.get(this)); // Bo can be reference or loaded
		}
 	}
		
	private Time _modified;
	public Time getModifiedTime() { return _modified; }
	protected void _setState(DomainData dd, BoInfo bi, List<Object> row) throws Exception {
		_modified = row.get(1)==null?null:new Time((java.util.Date)row.get(1));
		//String host = (String)row.get(2);
		version = (Integer)row.get(3);
		_setFlds(dd,bi,row,countSelectHeaderFlds());
	}

	/**
	 * Read current version from db immediately.
	 */
	public void refresh() {
		DBPool db = DBPool.getPool();
		DBConnection c = db.getConnection();
		DomainData dd = DomainCore.cd();
		try {
			c.prepare("select "+_getSelectFlds()+" from "+table() +" where id_f=?");
			c.param(id());
			List<List> rows = c.select();
			if (rows.size() == 1) {
				List row = rows.get(0);
				// set state
				Bo o = origin;
				o._modified = row.get(1)==null?null:new Time((java.util.Date)row.get(1));
				//String host = (String)row.get(2);
				o.version = (Integer)row.get(3);
				o._setFlds(dd,bi(),row,countSelectHeaderFlds());
				// copy fields to copy
				o.copy(bi(),this);
			}
			c.commit();
		} catch (Exception e) {
			try { c.rollback(); fail(e); } catch (Exception e2) { fail(e2); }
		} finally {
			try { c.free(); } catch (Exception e2) { fail(e2); }
		}
	}
	
	/**
	 * Write to db immediately.
	 */
	public void flush() {
		DBPool db = DBPool.getPool();
		DBConnection c = db.getConnection();		
		try {
			this._dbWriteFlds(c, bi());
			c.commit();
			modified = false; // not modified anymore
			// _modified Time still remains
			incrVersion();
		} catch (Exception e) {
			try { c.rollback(); fail(e); } catch (Exception e2) { fail(e2); }
		} finally {
			try { c.free(); } catch (Exception e2) { fail(e2); }
		}		
	}
	
	protected void _setFlds(DomainData dd, BoInfo bi, List row, int idx) throws Exception {
		for (Field f : bi.flds) {
			Object val = row.get(idx++);
			if (f.getType() == Time.class) {
				val = val==null?null:new Time((java.util.Date)val);				
			} else if (f.getType() == double.class) {
				val = val==null?0.0:((BigDecimal)val).doubleValue();	
			} else if (f.getType() == Money.class) {
				val = val==null?null:new Money((Integer)val);	
			} else if (f.getType() == boolean.class) {
				val = val==null?false:val;	
			} else if (hasInterface(f.getType(),EnumType.class)) {
				if (val != null) {
					val = Proxy.staticCall(f.getType(), "map",(Integer)val);
				}
			}
			f.set(this, val);
		}
		for (Field f : bi.cn1s) {
			Object val = row.get(idx++);
			if (val == null) {
				setOriginN1(f,null);
			} else {
				long id = (Integer)val;
				if (dd != null) {
					Bo bo = dd.getOrCreate((Class<Bo>)f.getType(), id);
					setOriginN1(f,bo);
				}
			}
		}
		if (dd != null) onLoad((IDomain)dd.d()); // call onLoad
	}

	protected String table() {
		Class<?> cl = this.getClass().getSuperclass();
		DaimsObject e = cl.getAnnotation(DaimsObject.class);
		return e.table();
	}

	protected void _dbWriteFlds(DBConnection c, BoInfo bi) throws Exception {
		// is it insert or update?
		if (isNew()) {
			//print("INSERT: " + this + " " + isNew() +","+isCopy()+","+isModified());
			c.prepare("insert into "+ bi.table+" set id_f=?,host=?,vers=?," 
					+ bi.updateFlds);
			c.param(id());
			c.param(DomainCore.getHostname());
			c.param(1L);
			_setWriteParams(c,bi);
			c.execute();
			version = 1L;
		} else {
			if (isModified()) {
				//print("UPDATE: " + this);
				c.prepare("update "+ bi.table+" set host=?,vers=?," 
						+ bi.updateFlds + " where id_f=? AND vers=?");
				c.param(DomainCore.getHostname());
				c.param(version+1L); // increment db version			
				_setWriteParams(c,bi);
				c.param(Long.parseLong(getId()));
				c.param(version);
				int res = c.execute();
				if (res != 1) {
					fail("object not found: " + bi.table + " " + getId() + " " + version);
				}
			}
			// increment mem object versions when db commit is ok
		}
	}


	protected void writeAllNNToDB(DBConnection c, BoInfo bi) throws Exception {
		int i = 0;
		for (Field f : bi.nns) {
			String nntable = bi.nntables.get(i++);
			BoMap<Bo> bm = this.getBoMap(f.getName());
			long oid1 = id();
			for (Bo bo : bm) {
				long oid2 = bo.id();
				//log.debug("writeNN to DB: " + oid1 + " " + oid2);
				if (oid1 == 0 || oid2 == 0) fail("mapping has null value");
				c.prepare("insert into " + nntable + " set o1=?,o2=?");
				//c.param(oid1);
				//c.param(oid2);
				c.param(oid2); // SWAP - old ANG project needs this
				c.param(oid1);
				c.execute();
			}
		}
	}
	
	public void delete() {
		onDelete();
		uow().delete(this);
	}

	protected BoInfo bi() {		
		BoInfo bi = new BoInfo();
		Bo o = this;
		bi.table = o.table();
		bi.updateFlds = o._getUpdateFlds();
		bi.flds = o.flds();
		bi.cn1s = o.cn1s();
		bi.cols = o.cols();
		bi.c1ns = o.c1ns();
		bi.nns = o.nns();
		for (Field f : bi.nns) {
			bi.nntables.add(f.getAnnotation(ConnectionNN.class).table());			
		}
		for (Field f : bi.flds) {
			bi.sizes.add(f.getAnnotation(Fld.class).size());
		}
		return bi;
	}
	

	protected void stats(Stats st, BoInfo bi) {
		for (Field f : bi.cols) {
			try {
				BoCollection bc = (BoCollection)f.get(this);
				st.addCollStats((Class<Bo>)getClass(), f.getName(), bc.size());
			} catch (Exception e) { fail(e); }
		}
	}
	
	
	// ------ life cycle callbacks ------
	protected void onDelete() {}
	protected void onDeleteEvent() {}
	protected void onLoad(IDomain d) {}
	protected void onSave() {}
	
	
	// ---------- IDO ------------
	// TODO: clean this IDO away
	public boolean equals(IDO o) { return equals((Object)o); }
	public DOH getDoh() { return new DOH(this.getClass().getName(),getId()); }
	public void delete(boolean ondelete) { }	
	public boolean matches(Object spec) { return false; }
	public String txtIdx(String lang) { return ""; }

	
	// ----- sql interface ------
	public void archive() {
		//setArchived(false);
		isArchived = true;
		uow().archive(this);
	}
	
	public void restore() {
		setArchived(false);
		refresh();
	}	
	
	private void setArchived(boolean v) {
		isArchived = v;
		String a = "0";
		if (v) a = "1";
		String sql = "update " + table() + " set archived="+a+" where id_f="+getId();
		DBPool db = DBPool.getPool();
		DBConnection c = db.getConnection();
		try {
			c.update(sql);
			c.commit();
		} catch (Exception e) {
			log.error("dbread failed",e);
			try { c.rollback(); } catch (Exception e2) { fail(e2);}
		} finally {
			try { c.free(); } catch (Exception e) {}
		}		
	}

}
